Skip to content

Conversation

PerikiyoXD
Copy link
Contributor

@PerikiyoXD PerikiyoXD commented Aug 23, 2025

Extract and refactor FPSLimiter to use a better FPS limiting logic while reducing CPU consumption

Queue-only ApplyFrameRateLimit, centralized pacing in ApplyQueuedFrameRateLimit

Caveats: Main menu runs with arbitrary FPS limit now (until we figure out hot to fps limit that codepath)

  • EnsureFrameRateLimitApplied no longer calls ApplyFrameRateLimit() from Present path to avoid skewed pacing; now only queues the target rate for the timer hook.
  • ApplyFrameRateLimit simplified to just queue the requested rate.
  • ApplyQueuedFrameRateLimit is now the single enforcement point: uses high-precision timing, adaptive sleep/yield calibration, and minimal spin to achieve stable pacing without wasting CPU.
  • Added detailed comments clarifying responsibilities and caveats (e.g. Main Menu not using timer hook).

Before you go ahead and create a pull request, please make sure:

@PerikiyoXD
Copy link
Contributor Author

Tests on 5800X + 3080Ti

FPS Cap 1.6 Release Build (CPU) #4385 changes Debug Build (CPU)
144 7.2% ± 0.3% 2.0% ± 0.2%
60 5.0% ± 0.2% 1.0% ± 0.2%
30 2.8% ± 0.2% 0.9% ± 0.2%
Uncapped 9.3% @ 450fps 7.8% @ 922fps

@ArranTuna
Copy link
Collaborator

ArranTuna commented Aug 23, 2025

Your chart got my interest, so I took some readings of my own before and with your PR and... holy shit with FPS limited, CPU usage was approximately halved.

FPS CPU GPU
Normal unlimited (197) 11% 34%
PR unlimited (197) 11% 29%
Normal limited 60 9.2% 25%
PR limited 60 4.7% 18%

So for me didn't make any difference when using unlimited FPS. But amazing reduction in CPU and GPU usage when limited.

Is this CPU usage now freed up so that players with low performance systems are now potentially going to get a much better FPS because that CPU time is now available to actually compute things, or no? I dunno just seems to good to be true because so many MTA players don't have good PCs so this change would be monumental for them.

@ArranTuna
Copy link
Collaborator

I've done some further testing, with some high CPU load:

crun setTimer(function() for i=1, 10000 do getElementData(localPlayer, "a") end end, 50, 0)

CPU and FPS were the same with and without this PR when under heavy load, when FPS was limited. However when FPS was set to a max of 60, it remained 60 FPS with the PR, but was 50 FPS on standard. Though is it possible although FPS may say it's higher it might not be better in the sense that the 10 extra frames may have tiny gaps between them and so some could end up being unbalanced in terms of feeling of smoothness.

@PerikiyoXD
Copy link
Contributor Author

PerikiyoXD commented Aug 23, 2025

Is this CPU usage now freed up so that players with low performance systems are now potentially going to get a much better FPS because that CPU time is now available to actually compute things, or no?

Yes. The old limiter wasted CPU cycles in a busy-wait loop after finishing a frame. That didn’t steal time from MTA's or SA’s own logic/rendering, but it did keep one core artificially busy doing nothing.

TL;DR: I replaced a spinlock (Tight loop calling Sleep) with a mixed approach to not burn so many CPU cycles

Your chart got my interest, so I took some readings of my own before and with your PR and... holy shit with FPS limited, CPU usage was approximately halved.
FPS CPU GPU
Normal unlimited (197) 11% 34%
PR unlimited (197) 11% 29%
Normal limited 60 9.2% 25%
PR limited 60 4.7% 18%

It'd be nice to know CPU + GPU combo

@ArranTuna
Copy link
Collaborator

It'd be nice to know CPU + GPU combo

i7-10700K + GTX 1070

My readings were based on a rough average of what task manager said.

@PerikiyoXD
Copy link
Contributor Author

Is this CPU usage now freed up so that players with low performance systems are now potentially going to get a much better FPS because that CPU time is now available to actually compute things, or no?

Short answer: kinda, but it's more about not wasting CPU cycles than giving you free performance.

Long answer:

The old frame limiter was genuinely awful (Sorry OG code dev). Look at this mess:

while (true) {
    double dSpare = dTargetTimeToUse - m_FrameRateTimer.Get();
    if (dSpare <= 0.0) break;
    if (dSpare >= 10.0)
        Sleep(1);  // Only sleep if >= 10ms left, otherwise just spin
}

So let's walk through what happened on a typical frame. You want 30fps (33.3ms), game renders in, let's say 4ms. Now you need to kill 29ms somehow to fit the frame target.

First it burns through 19ms calling Sleep(1) over and over... that's 19 separate system calls. Then when you're down to the last 9ms? It just sits there spinning the CPU, constantly checking m_FrameRateTimer.Get() in a tight loop until time runs out. This is the CPU burning I'm fixing.

Also, for lower FPS target, the more wasted cycles because you're waiting longer. 30fps wastes way more CPU than 144fps.

It's true that on lower end systems "render" time can be longer ms and it'd need to fill less, but the fact remains true for the remaining time.

The new approach is actually intelligent:

// Learn the sleep overhead, then sleep for most of the wait in ONE call
if (remaining_ms > sleep_overhead + 0.5) {
    Sleep(remaining_ms - learned_overhead);
}
// For medium waits, yield instead of spinning  
else if (remaining_ms > yield_overhead + 0.05) {
    Sleep(0);
}
// Only spin for the final microseconds where precision matters
if (still_need_to_wait) {
    if (remaining > 0.02ms) {
        for (int i = 0; i < 10; i++) _mm_pause();  // Batched pauses
    } else {
        _mm_pause();  // Ultra-tight final loop
    }
}

So, ONE bulk Sleep, then Sleep(0) for medium waits, and finally _mm_pause() for the last few microseconds.

One of the key insights is that it measures how long Sleep() actually takes on your machine and it adapts. Instead of guessing "Sleep(1) takes 1ms", it tracks the real overhead and somewhat compensates.

Plus it uses QueryPerformanceCounter for microsecond precision instead of that janky millisecond timer.

Will this give you +90 FPS? Nah. But it stops your CPU from pointlessly burning cycles just to wait around. The freed up CPU time goes back to the OS for other stuff like better multitasking, less heat, more headroom for background processes.

If you were already CPU bottlenecked somewhere else, this won't magically fix that. But if frame limiting was eating cycles for no reason, yeah those cycles are available now.

I dunno just seems to good to be true because so many MTA players don't have good PCs so this change would be monumental for them.

I get the skepticism but honestly this was just really bad code that got fixed.

Think about it: if you're running 30fps on a potato PC, the old limiter was literally doing 30+ unnecessary system calls per frame just to wait around, then spinning the CPU for whatever was left.

That's like 900+ wasted Sleep() calls per second plus constant busy-waiting IN A SECOND.

On weak hardware it actually matters more because every wasted cycle hurts.

The improvement scales with how bad your FPS cap is...

So yeah, players stuck at 30fps because their PC sucks will see the biggest benefit. Not necessarily higher FPS, but way less pointless CPU waste that was competing with the actual game.

@PerikiyoXD
Copy link
Contributor Author

PerikiyoXD commented Aug 23, 2025

On the other part, there is this issue with how MTA tried to frame-limit... It had two codepaths and I disabled one, with no real impact BUT the main menu rendering limit, which doesn't use the CTimer as the game is "technically" not using the timer until it's connected.

I need to find a proper way to fix the other codepath...

@Proxy-99 Proxy-99 mentioned this pull request Aug 24, 2025
1 task
@PlatinMTA
Copy link
Contributor

More benchmarks if you care, ran at 1600x900 windowed
CPU: Ryzen 7 7800X3D
GPU: RTX 5070

FPS Cap 1.6 1.7 with PR
Unlimited 12%~ @ 550fps 12%~ @ 550fps
144 9,8%~ 4,0%~
100 9,3%~ 3,0%~
60 7,0%~ 2,0%~
30 4,5%~ 1,2%~

NOTE: I had to wait like 5 to 10 minutes before the CPU usage dropped on 1.6 release (before it was at least 10% usage all the time), meanwhile this didn't happen with the PR. More over this dropped my unlimited FPS from 550fps to 480fps.

No difference in FPS disabling the cap, but the CPU usage is noticeable smaller for the capped results.

@ArranTuna
Copy link
Collaborator

That 10% constant usage you speak of is something I've seen before, it seems to be a random thing.

If this PR is complete I think you need to mark it as not a draft, so that it will get looked at.

@PerikiyoXD
Copy link
Contributor Author

If this PR is complete I think you need to mark it as not a draft, so that it will get looked at.

Still a draft as it has a huge BUT. The main menu don't get proper limitations.

@PerikiyoXD
Copy link
Contributor Author

That 10% constant usage you speak of is something I've seen before, it seems to be a random thing.

Probably MTA doing might and magic checks for it's own safety related to anticheat measures or release nitpicks... Can't tell really as I didn't really want to deep dive and risk a ban.

For a more fair comparison you can try:

  1. Git clone and Build master: Save as baseline. Keep the commit hash for reference.
  2. Apply patch (4385.diff): Rebuild & save as patched.
  3. Run both under identical conditions: Same server, same map. I used Map Editor for ease of use.
  4. Measure CPU usage: idle + load. I used System Informer for coarse graph plot.

Caution

(WARNING: System Informer triggers AC# 4! Adjust editor.conf if pertinent <disableac>4</disableac>. Don't use on Release or you'll get kicked automatically or risk a permaban)

  1. Compare baseline vs patched: And see if it's improved. Must be relatively near to current measurements.

@PerikiyoXD PerikiyoXD force-pushed the frame-limiter-refactor branch from ad2d4a0 to 73c70a6 Compare September 7, 2025 14:19
@PerikiyoXD
Copy link
Contributor Author

PerikiyoXD commented Sep 7, 2025

I've finished the relevant changes.
I added some goodies and TODOs that don't really harm and might need a little inspection for later refactors along the way.

FPS metrics

Tested idling in Ganton with no resources and no vehicles around. "stopall" basically.

Main Menu

FPS CPU Usage
144FPS 1.55%~
60FPS 0.97%~
30FPS 0.55%~

Ingame

FPS CPU Usage
144FPS 3.2%~
60FPS 1.6%~
30FPS 1.2%~

Test Request: @PlatinMTA @ArranTuna and anyone not mentioned that wants to help

@PerikiyoXD PerikiyoXD marked this pull request as ready for review September 7, 2025 14:27
@PerikiyoXD PerikiyoXD changed the title Efficiency upgrade on frame limiting Refactor framerate limiter to reduce CPU consumption Sep 7, 2025
@PerikiyoXD
Copy link
Contributor Author

PerikiyoXD commented Sep 7, 2025

New Features

VSync Frame Limiting

The game can now synchronize its frame rate with your monitor’s refresh rate.

  • Enable: vsync = 1
  • Disable: vsync = 0
  • Default: 1 (enabled)

New CVar: vsync

  • Purpose: Locks the frame rate to the monitor’s refresh rate when enabled.
  • Default: 1

Frame Limiter

Frame rate limits are applied in this priority order:

  1. Server Enforced – Hard cap set by the server
  2. Client Enforced – Explicit cap set by the client
  3. Client Preferred – Preferred cap via fps_limit CVar
  4. VSync – Falls back to monitor refresh rate if no stricter cap applies

Rule: The lowest active limit always takes precedence.

API Functions

  • setFPSLimit(limit) – server or client enforced (unchanged)
  • fps_limit CVar – client preferred (unchanged)
  • vsync CVar – enables/disables monitor sync (new)

Examples

Frame Limiter Examples

Server Client Preferred VSync (Hz) Final FPS Notes
60 - 144 144 60 Server limit overrides all
90 120 144 144 90 Server (90) is lowest active limit
- 120 144 144 120 Client enforced active, no server limit
- - 200 144 144 VSync limits FPS to monitor refresh
- - - 75 75 Only VSync active
- - 300 Disabled 300 Only client preferred limit active
30 60 120 144 30 Extreme server limit overrides all
120 90 144 144 90 Client enforced (90) is lowest
- - 90 60 60 VSync lower than client preferred → VSync wins
- 150 120 144 120 Client preferred (120) is lowest
200 180 150 144 144 VSync (144) is lowest among all limits
- - - Disabled Unlimited No limits active, VSync off

@PerikiyoXD
Copy link
Contributor Author

Possible later improvements to add:

  • Settings toggle for vsync

@Nico8340
Copy link
Member

Nico8340 commented Sep 7, 2025

Possible later improvements to add:

* Settings toggle for `vsync`

Would be quite useful and shouldn't be too hard to implement. That said, the average user likely isn't very familiar with CVARs or how to use them.

@FileEX
Copy link
Member

FileEX commented Sep 7, 2025

Possible later improvements to add:

  • Settings toggle for vsync

Since you've added such a CVAR, it's your responsibility to add the corresponding setting in the menu. I think the PR is incomplete without it.

@FileEX FileEX added the enhancement New feature or request label Sep 7, 2025
@PerikiyoXD PerikiyoXD requested a review from FileEX September 7, 2025 20:02
@PerikiyoXD
Copy link
Contributor Author

Added VSync option to settings:
imagen

@PerikiyoXD

This comment was marked as resolved.

…acing in ApplyQueuedFrameRateLimit

TL;DR: Main menu runs with arbitrary FPS limit now

- EnsureFrameRateLimitApplied no longer calls ApplyFrameRateLimit() from Present path
  to avoid skewed pacing; now only queues the target rate for the timer hook.
- ApplyFrameRateLimit simplified to just queue the requested rate.
- ApplyQueuedFrameRateLimit is now the single enforcement point:
  uses high-precision timing, adaptive sleep/yield calibration, and minimal spin
  to achieve stable pacing without wasting CPU.
- Added detailed comments clarifying responsibilities and caveats
  (e.g. Main Menu not using timer hook).
Replace scattered frame rate limiting logic with centralized `FPSLimiter` class.

**Key improvements:**
- High-precision timing using `RDTSC`, `QueryPerformanceFrequency`, and waitable timers
- Unified FPS limit handling from server, client scripts, user settings, and VSync
- Dynamic CEF browser frame rate synchronization
- Consistent frame pacing through `CModManager` pulse integration

**Changes:**
- Add `FPSLimiter` class with centralized frame limiting logic
- Integrate limiter into `CCore`, replacing old methods and variables
- Refactor `CCommands` to handle `fps_limit` and `vsync` cvars
- Update all APIs, Lua functions, and network packets to use `FPSLimiterInterface`
…tic_cast, and more.

- Cleanup CClientVariables::ValidateValues
- Removed // Easter egg! comment
Given that I needed to touch I applied formatting
@PerikiyoXD PerikiyoXD force-pushed the frame-limiter-refactor branch from 5c27d62 to a41e948 Compare September 8, 2025 13:16
@PerikiyoXD
Copy link
Contributor Author

Rebased branch to provide a clean linear history on top of latest master

@PerikiyoXD
Copy link
Contributor Author

Tested locally, compiles successfully, server runs normally, and all modified functionality works as expected... It feels done.

@PerikiyoXD PerikiyoXD requested a review from FileEX September 8, 2025 14:24
@PerikiyoXD PerikiyoXD force-pushed the frame-limiter-refactor branch from 2fc5432 to d6f120d Compare September 8, 2025 19:33
@ArranTuna
Copy link
Collaborator

I've tested it.

Saves a lot of CPU when minimized. CPU usage is 1% instead of 4%.

@Dutchman101
Copy link
Member

Thanks @PerikiyoXD, this is huge. I've asked for an additional MTA team opinion and we'll be merging it now, so it can be tested before the release of 1.7

@Dutchman101 Dutchman101 merged commit dde9a7f into multitheftauto:master Sep 18, 2025
MTABot pushed a commit that referenced this pull request Sep 18, 2025
dde9a7f Refactor framerate limiter to reduce CPU consumption (#4385)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request refactor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants